两行代码的库引发“血案”:坑了数百万个项目
4月25日,一个体量很小的 JavaScript 库is-promise 进行了更新。
当笔者看到这个新闻,心中不由得一颤,因为司徒正美老师生前一直奋斗的前端技术领域,当我来写这篇文章时,执笔当哭,缅怀旧人。
笔者和司徒结识于CSDN,记得是去年6月,当时我看到一篇博文《前端开发 20 年变迁史》,心中不由赞叹,竟有人能将前端技术上升到哲学高度来进行阐释,于是千方百计地找到了司徒的微信加为好友,和司徒聊天中,明显能感受到他对于前端技术的理解深度和积极热情。谁知天有不测风云,司徒年纪轻轻竟然溘然长逝,在此笔者也提示各位读者朋友们保重身份,切莫透支健康。
下面笔者帮助大家解读一下这则新闻背后的技术细节。由于前端并不是笔者的领域,如有错漏还请各位指正。
初识promise
因为笔者对于JavaScript也不是特别了解,在初步学习后我看到了阻塞代码、非阻塞代码、事件驱动设计模式、事件生命周期、函数堆栈、事件队列等概念,以及polyfill、babel、angular、reactJS、Vue.JS 等框架。
JavaScript是单线程执行代码,但是由于具备非阻塞和回调机制,JavaScript也可以实现异步功能。于是有了Promise机制。
根据MDN上对于Promise机制的描述(https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise):Promise对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象。
示例及注释如下:
//创建一个Promise对象,定义resolve方法,在3000ms后执行。
const promise1 = new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('promise');
}, 3000);
});
//非阻塞执行promise1,在promise完成后此方法会被执行。
promise1.then(function(value) {
console.log(value);
// 3s后会输出promise
});
console.log(promise1);
//直接输出 [object Promise]
综上笔者认为可以将Promise理解成Java中的Callback,用以帮助JavaScript实现异步功能。
本次出现问题的is-promise包,作用是测试一个JavaScript对象是否为Promise的。is-promise包的代码其实非常短,其主要的功能实现代码只有两行而已。
前端开发人员在引入is-promise包之后,就能在自己的项目中引用它,而且is-promise包是基于MIT协议的,因此引用该项目,也不必须要求开源。
虽然只有两行代码,但is-promise 库却是当今最受欢迎的 JavaScript 软件包之一。据不完全统计,is-promise是700余个知名的JavaScript 库的依赖项,其影响项目数量至少超过300万,范围涵盖至封闭源 JavaScript 代码库和 JavaScript 生态系统中一些最大的项目。
其中包括有:Facebook 的 Create React App(用于创建 React 应用程序的标准模板)、谷歌的 Angular.js 框架、谷歌的 Firebasse-tools、亚马逊的 AWS Serverless CLI、Nuxt.js 和 AVA 等。
引发问题的ES 模块标准
is-promise 库之所以引发问题,关键在于没有遵循正确的ES模块标准,而提到ES模块标准(EMS),我们还要从最基本的概念聊起。我们知道JavaScript是一门动态的脚本化语言,它让前端页面的开发变得非常简单。
JavaScript的编程范式抽象成维护变量、赋值和计算操作。大量的代码用于操作变量,开发者需要懂得如何去组织和维护这些变量。JavaScript 提供了一种方式,即函数作用域。
在一个函数内只需要考虑这个函数的变量问题,不必去担心其他函数会操作这些变量。
随之带来的问题是变量无法共享,无法在不同的函数之间相互共享变量。如果想要在作用域外共享变量,只能通过外层作用域,或者全局作用域。
ES模块标准是提供了更好的方式来组织变量和函数,把相关的变量和函数组织到一起。具体就是将这些函数和变量放到一个模块作用域内,实现在模块间共享变量。与函数作用域不同的是,模块内部的变量实现了在其他模块内共享。
还可指定哪些变量、类或者函数可以共享。在其他模块中共享,被称为 export。这就出现了模块间的依赖,这是一种很明确的关系,当移除一个模块时可以准确地知道哪些模块会出错。一旦有了模块间导出和引用变量的能力,我们就可以将代码打成小包。然后就可以像乐高玩具那样组合,再组合。使用小模块就可以创建出各类应用。在使用模块的时候,其实就是在做一个依赖关系图。ESM的模块包括三个过程:
1、构建:下载,解析,然后把文件解析为模块记录;
2、实例化:为模块分内存空间(此时还没赋值),然后依照导入,导出语句把模块指向内存地址,这个过程叫链接;
3、运行(求值):运行代码的时候,才会给内存空间填充真实的值。
EMS则通过一系列的标准来确保相关代码可以实现上述模块化的功能。
is-promise v.2.2.0 版本却未遵循正确的 ES 模块标准。因此在其更新发布后,引用了is-promise在各个项目都在的构建链时出现了问题。
总结一下本文内容,Promise是Javascript中实现异步功能的重要机制,is-promise又是目前流行度最高的promise对象检测工具,而由于is-promise没有遵循正确的ES模块标准,使得其它引用了is-promise包的程序出现了问题。
最后引用司徒正美老师在《前端开发 20 年变迁史》的话来做结束:“当初JavaScript被误解为最糟糕的语言,时至今日它是最流行的语言,GitHub 60%的项目都是与JavaScript有关……任何可以使用JavaScript来编写的应用,最终会由JavaScript编写。”
愿前端开发之路越来越好!
npm地址:https://www.npmjs.com/package/p-is-promise
Github地址:https://github.com/sindresorhus/p-is-promise
更多精彩推荐
☞华为海思超越高通,一季度国内占有率第一;苹果 iOS 13.5 优化 Face ID;Ruby 2.4 结束支持 | 极客头条
☞程序员内功修炼系列:10 张图解谈 Linux 物理内存和虚拟内存